#include <jni.h>
#include <stdarg.h>
#include <string>
#include <jni_md.h>
#include <Hilog/log.h>
#include "tun2http.h"
#include <sys/resource.h>

JavaVM *jvm = NULL;
int pipefds[2];
pthread_t thread_id = 0;
pthread_mutex_t lock;

int loglevel =  LOG_WARN;

extern int max_tun_msg;
extern struct ng_session *ng_session;

bool pt_enable_doh = false;
jobject doh_util;

void log_harmony(LogLevel prio, const char *fmt, ...){
    va_list arg_ptr;
    va_start(arg_ptr, fmt);
    //HILOG_DEBUG(prio,fmt,arg_ptr);
    HiLogPrint(LOG_APP,prio,LOG_DOMAIN,"tun2http",fmt,arg_ptr);
    va_end(arg_ptr);
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_myapplication_slice_MainAbilitySlice_stringFromJNI(JNIEnv* env, jobject  obj) {
    std::string hello = "Hello from JNI C++ codes";
    int len = hello.size();
    jchar res[len];
    for (int i = 0; i < len; i++) {
        res[i] = (jchar)hello[i];
    }
    log_harmony(LOG_INFO, hello.c_str());
    return env->NewString(res, len);
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    jvm = vm;
    JNIEnv *env;
    void**penv  = reinterpret_cast<void**>(&env);
    if (vm->GetEnv(penv, JNI_VERSION_1_6) != JNI_OK) {
        log_harmony(LOG_INFO, "JNI load GetEnv failed");
        return -1;
    }

    // Raise file number limit to maximum
    struct rlimit rlim;
//    rlimit rlim;
    if (getrlimit(RLIMIT_NOFILE, &rlim))
        log_harmony(LOG_WARN, "getrlimit error %d: %s", errno, strerror(errno));
    else {
        rlim_t soft = rlim.rlim_cur;
        rlim.rlim_cur = rlim.rlim_max;
        if (setrlimit(RLIMIT_NOFILE, &rlim))
            log_harmony(LOG_WARN, "setrlimit error %d: %s", errno, strerror(errno));
        else
            log_harmony(LOG_WARN, "raised file limit from %d to %d", soft, rlim.rlim_cur);
    }

    return JNI_VERSION_1_6;
}

void JNI_OnUnload(JavaVM *vm, void *reserved) {
    log_harmony(LOG_INFO, "JNI unload");
    JNIEnv *env;
    void**penv  = reinterpret_cast<void**>(&env);
    if (vm->GetEnv(penv, JNI_VERSION_1_6) != JNI_OK)
        log_harmony(LOG_INFO, "JNI load GetEnv failed");

}
//
//// JNI
//
bool get_jni_env(JNIEnv*(* env)) {
    bool did_attach_thread = false;
    *env = NULL;
    // Check if the current thread is attached to the VM
    auto get_env_result = jvm->GetEnv(reinterpret_cast<void**>(env), JNI_VERSION_1_6);
    if (get_env_result == JNI_EDETACHED) {
        if (jvm->AttachCurrentThread(reinterpret_cast<void**>(env), NULL) == JNI_OK) {
            did_attach_thread = true;
        }
    } else if (get_env_result == JNI_EVERSION) {
        // Unsupported JNI version. Throw an exception if you want to.
    }
    return did_attach_thread;
}

static jmethodID midProtect = NULL;


int protect_socket(const struct arguments *args, int socket) {
    jclass cls = args->env->GetObjectClass(args->instance);
    if (midProtect == NULL)
        midProtect = jniGetMethodID(args->env, cls, "protect", "(I)Z");

    jboolean isProtected = args->env->CallBooleanMethod(
            args->instance, midProtect, socket);
    jniCheckException(args->env);

    if (!isProtected) {
        log_harmony(LOG_ERROR, "protect socket failed");
        return -1;
    }

    args->env->DeleteLocalRef( cls);

    return 0;
}


jobject jniGlobalRef(JNIEnv *env, jobject cls) {
    jobject gcls = env->NewGlobalRef( cls);
    if (gcls == NULL)
        log_harmony(LOG_ERROR, "Global ref failed (out of memory?)");
    return gcls;
}

jclass jniFindClass(JNIEnv *env, const char *name) {
    jclass cls = env->FindClass( name);
    if (cls == NULL)
        log_harmony(LOG_ERROR, "Class %s not found", name);
    else
        jniCheckException(env);
    return cls;
}

jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature) {
    jmethodID method = env->GetMethodID( cls, name, signature);
    if (method == NULL) {
        log_harmony(LOG_ERROR, "Method %s %s not found", name, signature);
        jniCheckException(env);
    }
    return method;
}

int jniCheckException(JNIEnv *env) {
    jthrowable ex = env->ExceptionOccurred();
    if (ex) {
        env->ExceptionDescribe();
        env->ExceptionClear();
        env->DeleteLocalRef(ex);
        return 1;
    }
    return 0;
}





// JNI ServiceSinkhole
extern "C"
JNIEXPORT void JNICALL
Java_tun_proxy_service_Tun2HttpVpnService_jni_1init(JNIEnv *env, jobject instance) {
    loglevel = LOG_WARN;

    struct arguments args;
    args.env = env;
    args.instance = instance;
    init(&args);

    if (pthread_mutex_init(&lock, NULL))
        log_harmony(LOG_ERROR, "pthread_mutex_init failed");

    // Create signal pipe
    if (pipe(pipefds))
        log_harmony(LOG_ERROR, "Create pipe error %d: %s", errno, strerror(errno));
    else
        for (int i = 0; i < 2; i++) {
            int flags = fcntl(pipefds[i], F_GETFL, 0);
            if (flags < 0 || fcntl(pipefds[i], F_SETFL, flags | O_NONBLOCK) < 0)
                log_harmony(LOG_ERROR, "fcntl pipefds[%d] O_NONBLOCK error %d: %s",
                            i, errno, strerror(errno));
        }
}

JNIEXPORT void JNICALL
Java_tun_proxy_service_Tun2HttpVpnService_jni_1start(
        JNIEnv *env, jobject instance, jint tun, jboolean fwd53, jint rcode, jstring proxyIp, jint proxyPort, jboolean doh) {

    pt_enable_doh = (bool)(doh == JNI_TRUE);
    //doh_util = env->NewGlobalRef(env->FindClass("ru/krlvm/powertunnel/android/DOHUtility"));

    const char *proxy_ip = env->GetStringUTFChars(proxyIp, 0);

    max_tun_msg = 0;

    // Set blocking
    int flags = fcntl(tun, F_GETFL, 0);
    if (flags < 0 || fcntl(tun, F_SETFL, flags & ~O_NONBLOCK) < 0)
        log_harmony(LOG_ERROR, "fcntl tun ~O_NONBLOCK error %d: %s",
                    errno, strerror(errno));

    if (thread_id && pthread_kill(thread_id, 0) == 0)
        log_harmony(LOG_ERROR, "Already running thread %x", thread_id);
    else {
        jint rs = env->GetJavaVM( &jvm);
        if (rs != JNI_OK)
            log_harmony(LOG_ERROR, "GetJavaVM failed");

        // Get arguments
        struct arguments *args = (struct arguments *)malloc(sizeof(struct arguments));
        // args->env = will be set in thread
        args->instance = env->NewGlobalRef(instance);
        args->tun = tun;
        args->fwd53 = fwd53;
        args->rcode = rcode;
        strcpy(args->proxyIp, proxy_ip);
        args->proxyPort = proxyPort;


        // Start native thread
        int err = pthread_create(&thread_id, NULL, handle_events, (void *) args);
        if (err == 0)
            log_harmony(LOG_WARN, "Started thread %x", thread_id);
        else
            log_harmony(LOG_ERROR, "pthread_create error %d: %s", err, strerror(err));


    }

    env->ReleaseStringUTFChars(proxyIp, proxy_ip);
}

JNIEXPORT void JNICALL
Java_tun_proxy_service_Tun2HttpVpnService_jni_1stop(
        JNIEnv *env, jobject instance, jint tun) {
    pthread_t t = thread_id;
    log_harmony(LOG_WARN, "Stop tun %d  thread %x", tun, t);
    if (t && pthread_kill(t, 0) == 0) {
        log_harmony(LOG_WARN, "Write pipe thread %x", t);
        if (write(pipefds[1], "x", 1) < 0)
            log_harmony(LOG_WARN, "Write pipe error %d: %s", errno, strerror(errno));
        else {
            log_harmony(LOG_WARN, "Join thread %x", t);
            int err = pthread_join(t, NULL);
            if (err != 0)
                log_harmony(LOG_WARN, "pthread_join error %d: %s", err, strerror(err));
        }

        clear();

        log_harmony(LOG_WARN, "Stopped thread %x", t);
    } else
        log_harmony(LOG_WARN, "Not running thread %x", t);
}

JNIEXPORT jint JNICALL
Java_tun_proxy_service_Tun2HttpVpnService_jni_1get_1mtu(JNIEnv *env, jobject instance) {
    return get_mtu();
}


JNIEXPORT void JNICALL
Java_tun_proxy_service_Tun2HttpVpnService_jni_1done(JNIEnv *env, jobject instance) {
    log_harmony(LOG_INFO, "Done");

    clear();

    if (pthread_mutex_destroy(&lock))
        log_harmony(LOG_ERROR, "pthread_mutex_destroy failed");

    for (int i = 0; i < 2; i++)
        if (close(pipefds[i]))
            log_harmony(LOG_ERROR, "Close pipe error %d: %s", errno, strerror(errno));
}

// JNI Util

//JNIEXPORT jstring JNICALL
//Java_tun_utils_Util_jni_1getprop(JNIEnv *env, jclass type, jstring name_) {
//    const char *name = env->GetStringUTFChars( name_, 0);
//
//    char value[PROP_VALUE_MAX + 1] = "";
//    __system_property_get(name, value);
//
//    env->ReleaseStringUTFChars( name_, name);
//
//    return env->NewStringUTF( value);
//}
